home *** CD-ROM | disk | FTP | other *** search
- /*
- * DACPlayer.m
- * Implementation of an object to play sound over the soundout device (DACs).
- *
- * You may freely copy, distribute, and reuse the code in this example.
- * NeXT disclaims any warranty of any kind, expressed or implied, as to its
- * fitness for any particular use.
- *
- * Written by: Robert Poor
- * Edit History (most recent edits first):
- *
- * Version II created: Sep/92
- * Version I created Dec/89
- *
- * End of Edit History
- */
-
- #import <mach.h>
- #import <servers/netname.h>
- #import <appkit/Application.h>
- #import <appkit/Panel.h>
- #import <sound/accesssound.h>
- #import <sound/sounddriver.h>
- #import <sound/soundstruct.h>
- #import "DACPlayer.h"
- #import "errors.h"
- #import <c.h> /* for MAX */
-
- @interface DACPlayer(DACPlayerPrivate)
- - _setState:(Pla_state_t)newState;
- - _updateParameters;
- - _enqueueRegions;
- - _completedRegion;
- @end
-
- /*
- * Following are the routines that snddriver_reply_handler will
- * dispatch to. Note that in each case, the DACPlayer object itself
- * will be passed as the first argument.
- */
-
- static void DACCompletedMsg(void *arg, int tag)
- {
- [(id)arg _completedRegion];
- }
-
- /*
- * The dispatch table that snddriver_reply_handler uses.
- *
- * ### rpoor Sep 92: It appears that the sound driver doesn't post
- * ### underrun messages. I tried everything and never got notified...
- */
- static snddriver_handlers_t dacHandlers = {
- (void *)0, /* user-defined arg passed to dispatch functions */
- (int) 0, /* timeout */
- NULL, /* DACStartedMsg */
- DACCompletedMsg,
- NULL, /* DACAbortedMsg */
- NULL, /* DACPausedMsg */
- NULL, /* DACResumedMsg */
- NULL, /* DACUnderrunMsg */
- NULL,
- NULL,
- NULL,
- NULL
- };
-
- /*
- * HandleDACMessage is called courtesy of the DPSAddPort mechanism
- * whenever a message arrives from the sound driver. userData is bound
- * to the DACPlayer object itself.
- */
- static void HandleDACMessage(msg_header_t *msg, void *userData)
- {
- int r;
-
- dacHandlers.arg = userData; /* Install dacPlayer as arg to handlers */
- r = snddriver_reply_handler(msg, &dacHandlers);
- checkMachError((id)dacHandlers.arg, r, "Can't parse message from DAC");
- }
-
-
- @implementation DACPlayer:Object
-
- - init
- {
- self = [super init];
-
- playerState = PLA_STOPPED;
- regionsQueued = bytesPlayed = bytesQueued = 0;
- [self setSamplingRate:SND_RATE_HIGH];
- [self setRegionSize:4*vm_page_size andCount:3];
-
- return self;
- }
-
- - free
- {
- return [super free];
- }
-
- /***
- *** METHODS THAT CONTROL DACPLAYER
- ***/
-
- - prepare
- /*
- * grab all the required resources, queue up the initial regions
- * (this WILL call the delegate playData::: method), and set state
- * to PLA_PAUSED.
- */
- {
- port_t arbitration_port;
- int r, rate, protocol;
-
- [self stop]; /* make sure playing has stopped first. */
-
- /* get the ports that we need */
- r = SNDAcquire(SND_ACCESS_OUT, 0, 0, 0, NULL_NEGOTIATION_FUN,
- 0, &devicePort, &ownerPort);
- if (!checkMachError(self,r,"Can't acquire SoundOut resouces"))
- return nil;
-
- arbitration_port = ownerPort;
- r = snddriver_set_sndout_owner_port(devicePort,ownerPort,&arbitration_port);
- if (!checkSnddriverError(self,r,"Cannot become owner of sound-out resources"))
- return nil;
-
- r = snddriver_set_ramp(devicePort,0); /* disable ramping */
- checkSnddriverError(self,r,"Call to disable ramp failed"); /* not fatal */
-
- /*
- * Tell the delegate (if any) that we are about to start playing.
- * Call it here in case the delegate wants to configure any of the
- * dacPlayer parameters (samplingRate, regionSize, regionCount).
- */
- if (flags.willPlay) {
- [delegate willPlay :self];
- }
- [self _updateParameters];
-
- /* set up the DMA read stream */
- protocol = 0;
- rate = ((samplingRate == SND_RATE_HIGH)?
- SNDDRIVER_STREAM_TO_SNDOUT_44:
- SNDDRIVER_STREAM_TO_SNDOUT_22);
- r = snddriver_stream_setup(devicePort,
- ownerPort,
- rate,
- READ_BUF_SIZE,
- sizeof(short),
- LOW_WATER,
- HIGH_WATER,
- &protocol, /* ignored for sndout */
- &streamPort);
- if (!checkSnddriverError(self,r,"Cannot set up stream to sound-out"))
- return nil;
-
- /* allocate a port for the replies */
- r = port_allocate(task_self(),&replyPort);
- if (!checkMachError(self,r,"Cannot allocate reply port"))
- return nil;
-
- /* Start the DMA stream in a paused state */
- r = snddriver_stream_control(streamPort,0,SNDDRIVER_PAUSE_STREAM);
- if (!checkSnddriverError(self,r,"can't do initial pause"))
- return nil;
-
- /*
- * Queue up the initial buffers before starting.
- */
- bytesPlayed = 0;
- bytesQueued = 0;
- regionsQueued = 0;
- [self _enqueueRegions];
-
- /*
- * Set things up to call HandleDACMessage whenever we receive a
- * message on the replyPort. (We don't expect ro recieve any message
- * until we unpause the stream, see the -run method for that...)
- */
- DPSAddPort(replyPort,
- HandleDACMessage, /* function to call */
- MSG_SIZE_MAX,
- self, /* second arg to HandleDACMessage */
- NX_MODALRESPTHRESHOLD /* priority */
- );
-
- [self _setState:PLA_PAUSED];
-
- return self;
- }
-
- - run
- {
- int r;
-
- if (playerState == PLA_RUNNING) {
- return nil; /* already running. Ignore the bum */
- } else if (playerState == PLA_STOPPED) {
- [self prepare]; /* starting cold. prepare first then run */
- } else if (playerState == PLA_STOPPING) {
- [[self stop] prepare]; /* stop first, prepare, then run... */
- return nil;
- } /* else playerState == PLA_PAUSED */
-
- /*
- * At this point we know that state = PLA_PAUSED, any initial buffers
- * are already queued up, and the stream is paused. We simply resume
- * the stream to start things going...
- */
- r = snddriver_stream_control(streamPort,0,SNDDRIVER_RESUME_STREAM);
- if (!checkSnddriverError(self,r,"Can't resume the DMA stream"))
- return nil;
-
- return [self _setState:PLA_RUNNING];
- }
-
- - pause
- {
- int r;
-
- if (playerState == PLA_PAUSED) {
- return nil;
- } else if (playerState == PLA_STOPPED) {
- return [self prepare];
- } else if (playerState == PLA_STOPPING) {
- return [[self stop] prepare];
- } /* else playerState == PLA_RUNNING */
-
- r = snddriver_stream_control(streamPort,WRITE_TAG,SNDDRIVER_PAUSE_STREAM);
- checkSnddriverError(self,r,"Call to pause stream failed");
-
- return [self _setState:PLA_PAUSED];
- }
-
- - stop
- /*
- * Terminate all playing right now.
- */
- {
- int r;
-
- if (playerState == PLA_STOPPED) {
- return nil;
- }
-
- /* flush any outstanding buffers */
- r = snddriver_stream_control(streamPort,
- WRITE_TAG,
- SNDDRIVER_ABORT_STREAM);
- checkSnddriverError(self,r,"Couldn't abort stream");
- DPSRemovePort(replyPort);
-
- r = SNDRelease(SND_ACCESS_OUT, devicePort, ownerPort);
- if (!checkMachError(self,r,"Can't deallocate the owner port"))
- return nil;
- r = port_deallocate(task_self(),replyPort);
- if (!checkMachError(self,r,"Can't deallocate the reply port"))
- return nil;
-
- /*
- * Tell the delegate that we stopped playing.
- */
- if (flags.didPlay) {
- [delegate didPlay :self];
- }
-
- bytesPlayed = bytesQueued = 0; /* we've shut everything down */
- return [self _setState:PLA_STOPPED];
- }
-
-
- - finish
- /*
- * The finish method only sets the state to PLA_STOPPING. The routines
- * that handle requests from the sound driver will notice this and will
- * stop enqueuing new regions. When the last region has been played,
- * dacPlayer will call [self stop] to shut things down.
- */
- {
- if (playerState == PLA_STOPPED) {
- return nil; /* already stopped */
- } else if (playerState == PLA_STOPPING) {
- return nil; /* already stopping */
- } /* else (playerState == PLA_RUNNING) || (playerState == PLA_PAUSED) */
-
- return [self _setState:PLA_STOPPING];
- }
-
-
- /***
- *** METHODS THAT CONFIGURE DACPLAYER
- ***/
-
- - delegate { return delegate; }
- - setDelegate:anObject
- {
- delegate = anObject;
- /* make note of which methods the delegate responds do */
- flags.willPlay = [delegate respondsTo:@selector(willPlay:)];
- flags.didPlay = [delegate respondsTo:@selector(didPlay:)];
- flags.playData = [delegate respondsTo:@selector(playData:::)];
- flags.didChangeState =
- [delegate respondsTo:@selector(didChangeState:from:to:)];
- return self;
- }
-
- - (int)regionSize { return regionSize; }
- - (int)regionCount { return regionCount; }
- - setRegionSize:(int)size andCount:(int)count
- {
- int pages;
-
- /* round up to nearest vm_page_size boundary */
- pages = (size+vm_page_size-1)/vm_page_size;
- newRegionSize = MAX(1,pages)*vm_page_size;
- newRegionCount = MAX(1,count);
- /* update the new parameters if it's safe... */
- return [self _updateParameters];
- }
-
- - (int)samplingRate { return samplingRate; }
- - setSamplingRate:(int)aRate
- {
- if (aRate < (SND_RATE_HIGH+SND_RATE_LOW)/2) {
- newSamplingRate = SND_RATE_LOW;
- } else {
- newSamplingRate = SND_RATE_HIGH;
- }
- /* update the new parameters if it's safe... */
- return [self _updateParameters];
- }
-
- /***
- *** METHODS THAT QUERY DACPLAYER
- ***/
-
- - (Pla_state_t)playerState { return playerState; }
-
- #define BYTES_PER_SAMPLE (sizeof(short))
- #define BYTES_PER_FRAME (BYTES_PER_SAMPLE * 2)
-
- - (int)bytesPlayed { return bytesPlayed; }
- - (int)samplesPlayed { return bytesPlayed / BYTES_PER_SAMPLE; }
- - (int)framesPlayed { return bytesPlayed / BYTES_PER_FRAME; };
- - (double)secondsPlayed
- {
- return (double)bytesPlayed / (double)(samplingRate * BYTES_PER_FRAME);
- }
-
- - (int)bytesQueued { return bytesQueued; }
- - (int)samplesQueued { return bytesQueued / BYTES_PER_SAMPLE; }
- - (int)framesQueued { return bytesQueued / BYTES_PER_FRAME; }
- - (double)secondsQueued
- {
- return (double)bytesQueued / (double)(samplingRate * BYTES_PER_FRAME);
- }
-
-
- /***
- *** Internal methods
- ***/
-
- - _setState:(Pla_state_t)newState
- /*
- * Set the state to newState. If there's a delegate that wants to know
- * about the change in state, tell 'em. Returns self.
- */
- {
- Pla_state_t prevState = playerState;
-
- playerState = newState;
- if (flags.didChangeState) {
- [delegate didChangeState:self from:prevState to:newState];
- }
- return self;
- }
-
- - _updateParameters
- /*
- * If the state == PLA_STOPPED, update the "pending" new parameters.
- * We'll always call this method in -prepare before starting.
- */
- {
- if (playerState == PLA_STOPPED) {
- samplingRate = newSamplingRate;
- regionCount = newRegionCount;
- regionSize = newRegionSize;
- }
- return self;
- }
-
- - _enqueueRegions
- /*
- * This is the method that sends buffers to the sound driver stream.
- *
- * Unless state == STOPPING, it writes regions to the sound driver until
- * regionsQueued == regionCount. Each region is passed to the delegate
- * in a playData::: method before being handed off to the sound driver.
- */
- {
- int r;
- char *currentRegion;
-
- while ((playerState != PLA_STOPPING) && (regionsQueued < regionCount)) {
-
- /* allocate a region */
- r = vm_allocate(task_self(),(vm_address_t)currentRegion,regionSize,TRUE);
-
- /* let the delegate have its way with the regrion */
- if (flags.playData) {
- [delegate playData :self :currentRegion :regionSize];
- }
-
- /* enqueue the buffer in the DMA stream to the DACs */
- r = snddriver_stream_start_writing
- (streamPort, /* port */
- currentRegion, /* data to be played */
- regionSize/sizeof(short), /* number of samples to play */
- WRITE_TAG, /* tag for this region */
- FALSE, /* preempt */
- TRUE, /* deallocate on completion */
- FALSE, /* send msg when started */
- TRUE, /* send msg when completed */
- FALSE, /* send msg when aborted */
- FALSE, /* send msg when paused */
- FALSE, /* send msg when resumed */
- FALSE, /* send msg when underflowed */
- replyPort /* port for the above messages */
- );
- checkSnddriverError(self,r,"Cannot enqueue write request");
- regionsQueued += 1;
- bytesQueued += regionSize;
- }
- return self;
- }
-
- - _completedRegion
- /*
- * This method is called whenever we get a "region completed" message from
- * the sound driver. (How? The sound driver posts a message on replyPort,
- * which is noticed by the DPSAddPort mechanism which calls HandleDACMessage
- * which calls snddriver_reply_handler which dispatches to DACCompletedMessage
- * which (finally) calls _completedRegion.) Got that?
- *
- * Oh, what's this method DO? It makes note of the fact that another region
- * has been consumed by the DAC stream and calls _enqueueRegions to replenish
- * the region queue. If state == PLA_STOPPING and the region queue has hit
- * zero, it means that we've just played the last region and we call
- * [self stop] to shut things down.
- */
- {
- /* bump bytesPlayed to reflect the fact that the sound driver played 'em */
- bytesPlayed += regionSize;
-
- /* note that one region has been consumed by the sound driver. */
- regionsQueued -= 1;
-
- /* replenish the queue of regions */
- [self _enqueueRegions];
-
- /* shut down the machinery if we're stopping */
- if ((playerState == PLA_STOPPING) && (regionsQueued == 0)) {
- [self stop];
- }
- return self;
- }
-
-
- @end
-